// Returns true if it launched at least one thread, and false otherwise.
// aSleepDuration can be be zero to do a true Sleep(0), or less than 0 to avoid sleeping or
// waiting at all (i.e. messages are checked and if there are none, the function will return
// immediately). aMode is either RETURN_AFTER_MESSAGES (default) or WAIT_FOR_MESSAGES.
// If the caller doesn't specify aSleepDuration, this function will return after a
// time less than or equal to SLEEP_INTERVAL (i.e. the exact amount of the sleep
// isn't important to the caller). This mode is provided for performance reasons
// (it avoids calls to GetTickCount and the TickCount math). However, if the
// caller's script subroutine is suspended due to action by us, an unknowable
// amount of time may pass prior to finally returning to the caller.
{
bool we_turned_on_defer = false; // Set default.
if (aMode == RETURN_AFTER_MESSAGES_SPECIAL_FILTER)
{
aMode = RETURN_AFTER_MESSAGES; // To simplify things further below, eliminate the mode RETURN_AFTER_MESSAGES_SPECIAL_FILTER from further consideration.
// g_DeferMessagesForUnderlyingPump is a global because the instance of MsgSleep on the calls stack
// that set it to true could launch new thread(s) that call MsgSleep again (i.e. a new layer), and a global
// is the easiest way to inform all such MsgSleeps that there's a non-standard msg pump beneath them on the
// call stack.
if (!g_DeferMessagesForUnderlyingPump)
{
g_DeferMessagesForUnderlyingPump = true;
we_turned_on_defer = true;
}
// So now either we turned it on or some layer beneath us did. Therefore, we know there's at least one
// non-standard msg pump beneath us on the call stack.
}
// The following is done here for performance reasons. UPDATE: This probably never needs
// to close the clipboard now that Line::ExecUntil() also calls CLOSE_CLIPBOARD_IF_OPEN:
CLOSE_CLIPBOARD_IF_OPEN;
// While in mode RETURN_AFTER_MESSAGES, there are different things that can happen:
// 1) We launch a new hotkey subroutine, interrupting/suspending the old one. But
// subroutine calls this function again, so now it's recursed. And thus the
// new subroutine can be interrupted yet again.
// 2) We launch a new hotkey subroutine, but it returns before any recursed call
// to this function discovers yet another hotkey waiting in the queue. In this
// case, this instance/recursion layer of the function should process the
// hotkey messages linearly rather than recursively? No, this doesn't seem
// necessary, because we can just return from our instance/layer and let the
// caller handle any messages waiting in the queue. Eventually, the queue
// should be emptied, especially since most hotkey subroutines will run
// much faster than the user could press another hotkey, with the possible
// exception of the key-repeat feature triggered by holding a key down.
// Even in that case, the worst that would happen is that messages would
// get dropped off the queue because they're too old (I think that's what
// happens).
// Based on the above, when mode is RETURN_AFTER_MESSAGES, we process
// all messages until a hotkey message is encountered, at which time we
// launch that subroutine only and then return when it returns to us, letting
// the caller handle any additional messages waiting on the queue. This avoids
// the need to have a "run the hotkeys linearly" mode in a single iteration/layer
// of this function. Note: The WM_QUIT message does not receive any higher
// precedence in the queue than other messages. Thus, if there's ever concern
// that that message would be lost, as a future change perhaps can use PeekMessage()
// with a filter to explicitly check to see if our queue has a WM_QUIT in it
// somewhere, prior to processing any messages that might take result in
// a long delay before the remainder of the queue items are processed (there probably
// aren't any such conditions now, so nothing to worry about?)
// Above is somewhat out-of-date. The objective now is to spend as much time
// inside GetMessage() as possible, since it's the keystroke/mouse engine
// whenever the hooks are installed. Any time we're not in GetMessage() for
// any length of time (say, more than 20ms), keystrokes and mouse events
// will be lagged. PeekMessage() is probably almost as good, but it probably
// only clears out any waiting keys prior to returning. CONFIRMED: PeekMessage()
// definitely routes to the hook, perhaps only if called regularly (i.e. a single
// isolated call might not help much).
// This var allows us to suspend the currently-running subroutine and run any
// hotkey events waiting in the message queue (if there are more than one, they
// will be executed in sequence prior to resuming the suspended subroutine).
// Never static because we could be recursed (e.g. when one hotkey iterruptes
// a hotkey that has already been interrupted) and each recursion layer should
// have it's own value for this:
global_struct global_saved;
char ErrorLevel_saved[ERRORLEVEL_SAVED_SIZE];
// Decided to support a true Sleep(0) for aSleepDuration == 0, as well
// as no delay at all if aSleepDuration < 0. This is needed to implement
// "SetKeyDelay, 0" and possibly other things. I believe a Sleep(0)
// is always <= Sleep(1) because both of these will wind up waiting
// a full timeslice if the CPU is busy.
// Reminder for anyone maintaining or revising this code:
// Giving each subroutine its own thread rather than suspending old ones is
// probably not a good idea due to the exclusive nature of the GUI
// (i.e. it's probably better to suspend existing subroutines rather than
// letting them continue to run because they might activate windows and do
// other stuff that would interfere with the window automation activities of
// other threads)
// If caller didn't specify, the exact amount of the Sleep() isn't
// critical to it, only that we handles messages and do Sleep()
// a little.
// Most of this initialization section isn't needed if aMode == WAIT_FOR_MESSAGES,
// but it's done anyway for consistency:
bool allow_early_return;
if (aSleepDuration == INTERVAL_UNSPECIFIED)
{
aSleepDuration = SLEEP_INTERVAL; // Set interval to be the default length.
allow_early_return = true;
}
else
// The timer resolution makes waiting for half or less of an
// interval too chancy. The correct thing to do on average
// is some kind of rounding, which this helps with:
GuiType *pgui; // This is just a temp variable and should not be referred to once the below has been determined.
GuiControlType *pcontrol, *ptab_control;
GuiIndexType gui_control_index, gui_index; // gui_index is needed to avoid using pgui in cases where that pointer becomes invalid (e.g. if ExecUntil() executes "Gui Destroy").
if (aSleepDuration > 0 && !empty_the_queue_via_peek && !g_DeferMessagesForUnderlyingPump) // g_Defer: Requires a series of Peeks to handle non-contingous ranges, which is why GetMessage() can't be used.
{
// The following comment is mostly obsolete as of v1.0.39 (which introduces a thread
// dedicated to the hooks). However, using GetMessage() is still superior to
// PeekMessage() for performance reason. Add to that the risk of breaking things
// and it seems clear that it's best to retain GetMessage().
// Older comment:
// Use GetMessage() whenever possible -- rather than PeekMessage() or a technique such
// MsgWaitForMultipleObjects() -- because it's the "engine" that passes all keyboard
// and mouse events immediately to the low-level keyboard and mouse hooks
// Check the active window in each iteration in case a signficant amount of time has passed since
// the previous iteration (due to launching threads, etc.)
if (g_DeferMessagesForUnderlyingPump && (fore_window = GetForegroundWindow()) != NULL // There is a foreground window.
&& GetWindowThreadProcessId(fore_window, NULL) == g_MainThreadID) // And it belongs to our main thread (the main thread is the only one that owns any windows).
{
do_special_msg_filter = false; // Set default.
if (g_nFileDialogs) // v1.0.44.12: Also do the special Peek/msg filter below for FileSelectFile because testing shows that frequently-running timers disrupt the ability to double-click.
do_special_msg_filter = !strcmp(wnd_class_name, "#32770"); // Due to checking g_nFileDialogs above, this means that this dialog is probably FileSelectFile rather than MsgBox/InputBox/FileSelectFolder (even if this guess is wrong, it seems fairly inconsequential to filter the messages since other pump beneath us on the call-stack will handle them ok).
}
if (!do_special_msg_filter && (focused_control = GetFocus()))
// Since the Peek() didn't find any messages, our timeslice may have just been
// yielded if the CPU is under heavy load (update: this yielding effect is now diffcult
// to reproduce, so might be a thing of past service packs). If so, it seems best to count
// that as a "rest" so that 10ms script-timers will run closer to the desired frequency
// (see above comment for more details).
// These next few lines exact match the ones above, so keep them in sync:
tick_after = GetTickCount();
if (tick_after - tick_before > 3)
g_script.mLastScriptRest = tick_after;
// UPDATE: The section marked "OLD" below is apparently not quite true: although Peek() has been
// caught yielding our timeslice, it's now difficult to reproduce. Perhaps it doesn't consistently
// yield (maybe it depends on the relative priority of competing processes) and even when/if it
// does yield, it might somehow not as long or as good as Sleep(0). This is evidenced by the fact
// that some of my script's WinWaitClose's finish too quickly when the Sleep(0) is omitted after a
// Peek() that returned FALSE.
// OLD (mostly obsolete in light of above): It is not necessary to actually do the Sleep(0) when
// aSleepDuration == 0 because the most recent PeekMessage() has just yielded our prior timeslice.
// This is because when Peek() doesn't find any messages, it automatically behaves as though it
// did a Sleep(0).
if (aSleepDuration == 0 && !sleep0_was_done)
{
Sleep(0);
sleep0_was_done = true;
// Now start a new iteration of the loop that will see if we
// received any messages during the up-to-20ms delay (perhaps even more)
// that just occurred. It's done this way to minimize keyboard/mouse
// lag (if the hooks are installed) that will occur if any key or
// mouse events are generated during that 20ms. Note: It seems that
// the OS knows not to yield our timeslice twice in a row: once for
// the Sleep(0) above and once for the upcoming PeekMessage() (if that
// PeekMessage() finds no messages), so it does not seem necessary
// to check HIWORD(GetQueueStatus(QS_ALLEVENTS)). This has been confirmed
// via the following test, which shows that while BurnK6 (CPU maxing program)
// is foreground, a Sleep(0) really does a Sleep(60). But when it's not
// foreground, it only does a Sleep(20). This behavior is UNAFFECTED by
// the added presence of of a HIWORD(GetQueueStatus(QS_ALLEVENTS)) check here:
//SplashTextOn,,, xxx
//WinWait, xxx ; set last found window
//Loop
//{
// start = %a_tickcount%
// Sleep, 0
// elapsed = %a_tickcount%
// elapsed -= %start%
// WinSetTitle, %elapsed%
//}
continue;
}
// Otherwise: aSleepDuration is non-zero or we already did the Sleep(0)
// Macro notes:
// Must decrement prior to every RETURN to balance it.
// Do this prior to checking whether timer should be killed, below.
// Kill the timer only if we're about to return OK to the caller since the caller
// would still need the timer if FAIL was returned above. But don't kill it if
// there are any enabled timed subroutines, because the current policy it to keep
// the main timer always-on in those cases. UPDATE: Also avoid killing the timer
// if there are any script threads running. To do so might cause a problem such
// as in this example scenario: MsgSleep() is called for any reason with a delay
// large enough to require the timer. The timer is set. But a msg arrives that
// MsgSleep() dispatches to MainWindowProc(). If it's a hotkey or custom menu,
// MsgSleep() is called recursively with a delay of -1. But when it finishes via
// IsCycleComplete(), the timer would be wrongly killed because the underlying
// instance of MsgSleep still needs it. Above is even more wide-spread because if
// MsgSleep() is called recursively for any reason, even with a duration >10, it will
// wrongly kill the timer upon returning, in some cases. For example, if the first call to
// MsgSleep(-1) finds a hotkey or menu item msg, and executes the corresponding subroutine,
// that subroutine could easily call MsgSleep(10+) for any number of reasons, which
// would then kill the timer.
// Also require that aSleepDuration > 0 so that MainWindowProc()'s receipt of a
// WM_HOTKEY msg, to which it responds by turning on the main timer if the script
// is uninterruptible, is not defeated here. In other words, leave the timer on so
// that when the script becomes interruptible once again, the hotkey will take effect
// almost immediately rather than having to wait for the displayed dialog to be
// dismissed (if there is one).
// If timer doesn't exist, the new-thread-launch routine of MsgSleep() relies on this
// to turn it back on whenever a layer beneath us needs it. Since the timer
// is never killed while g_script.mTimerEnabledCount is >0, it shouldn't be necessary
// to check g_script.mTimerEnabledCount here.
//
// "we_turned_on_defer" is necessary to prevent us from turning it off if some other
// instance of MsgSleep beneath us on the calls stack turned it on. Only it should
// turn it off because it might still need the "true" value for further processing.
#define RETURN_FROM_MSGSLEEP \
{\
if (we_turned_on_defer)\
g_DeferMessagesForUnderlyingPump = false;\
if (this_layer_needs_timer)\
--g_nLayersNeedingTimer;\
if (g_MainTimerExists)\
{\
if (aSleepDuration > 0 && !g_nLayersNeedingTimer && !g_script.mTimerEnabledCount && !Hotkey::sJoyHotkeyCount)\
KILL_MAIN_TIMER \
}\
else if (g_nLayersNeedingTimer)\
SET_MAIN_TIMER \
return return_value;\
}
// IsCycleComplete should always return OK in this case. Also, was_interrupted
// will always be false because if this "aSleepDuration < 1" call really
// was interrupted, it would already have returned in the hotkey cases
// of the switch(). UPDATE: was_interrupted can now the hotkey case in
// the switch() doesn't return, relying on us to do it after making sure
// the queue is empty.
// The below is checked here rather than in IsCycleComplete() because
// that function is sometimes called more than once prior to returning
// (e.g. empty_the_queue_via_peek) and we only want this to be decremented once:
if (IsCycleComplete(aSleepDuration, start_time, allow_early_return)) // v1.0.44.11: IsCycleComplete() must be called for all modes, but now its return value is checked due to the new g_DeferMessagesForUnderlyingPump mode.
RETURN_FROM_MSGSLEEP
// Otherwise (since above didn't return) combined logic has ensured that all of the following are true:
// 1) aSleepDuration > 0
// 2) !empty_the_queue_via_peek
// 3) The above two combined with logic above means that g_DeferMessagesForUnderlyingPump==true.
Sleep(5); // Since Peek() didn't find a message, avoid maxing the CPU. This is a somewhat arbitrary value: the intent of a value below 10 is to avoid yielding more than one timeslice on all systems even if they have unusual timeslice sizes / system timers.
continue;
}
// else Peek() found a message, so process it below.
} // PeekMessage() vs. GetMessage()
// Since above didn't return or "continue", a message has been received that is eligible
// for further processing.
// For max. flexibility, it seems best to allow the message filter to have the first
// crack at looking at the message, before even TRANSLATE_AHK_MSG:
if (g_MsgMonitorCount && MsgMonitor(msg.hwnd, msg.message, msg.wParam, msg.lParam, &msg, msg_reply)) // Count is checked here to avoid function-call overhead.
{
continue; // MsgMonitor has returned "true", indicating that this message should be omitted from further processing.
// NOTE: Above does "continue" and ignores msg_reply. This is because testing shows that
// messages received via Get/PeekMessage() were always sent via PostMessage. If an
// another thread sends ours a message, MSDN implies that Get/PeekMessage() internally
// calls the message's WindowProc directly and sends the reply back to the other thread.
// That makes sense because it seems unlikely that DispatchMessage contains any means
// of replying to a message because it has no way of knowing whether the MSG struct
// arrived via Post vs. SendMessage.
}
// If this message might be for one of our GUI windows, check that before doing anything
// else with the message. This must be done first because some of the standard controls
// also use WM_USER messages, so we must not assume they're generic thread messages just
// because they're >= WM_USER. The exception is AHK_GUI_ACTION should always be handled
// here rather than by IsDialogMessage(). Note: sGuiCount is checked first to help
// performance, since all messages must come through this bottleneck.
// Pass false for both the above since that's the whole point of having arrow
// keys handled separately from the below: Focus should stay on the tabs
// rather than jumping to the first control of the tab, it focus should not
// wrap around to the beginning or end (to conform to standard behavior for
// arrow keys).
continue; // Suppress this key even if the above failed (probably impossible in this case).
}
//else fall through to the next part.
}
// If focus is in a multiline edit control, don't act upon Control-Tab (and
// shift-control-tab -> for simplicity & consistency) since Control-Tab is a special
// keystroke that inserts a literal tab in the edit control:
if ( msg.wParam != VK_LEFT && msg.wParam != VK_RIGHT
&& (GetKeyState(VK_CONTROL) & 0x8000) // Even if other modifiers are down, it still qualifies. Use GetKeyState() vs. GetAsyncKeyState() because the former's definition is more suitable.
// If ptab_control wasn't determined above, check if focused control is owned by a tab control:
if (!ptab_control && !(ptab_control = pgui->FindTabControl(pcontrol->tab_control_index)) )
// Fall back to the first tab control (for consistency & simplicty, seems best
// to always use the first rather than something fancier such as "nearest in z-order".
ptab_control = pgui->FindTabControl(0);
if (ptab_control)
{
pgui->SelectAdjacentTab(*ptab_control
, msg.wParam == VK_NEXT || (msg.wParam == VK_TAB && !(GetKeyState(VK_SHIFT) & 0x8000)) // Use GetKeyState() vs. GetAsyncKeyState() because the former's definition is more suitable.
, true, true);
// Update to the below: Must suppress the tab key at least, to prevent it
// from navigating *and* changing the tab. And since this one is suppressed,
// might as well suppress the others for consistency.
// Older: Since WM_KEYUP is not handled/suppressed here, it seems best not to
// suppress this WM_KEYDOWN either (it should do nothing in this case
// anyway, but for balance this seems best): Fall through to the next section.
continue;
}
//else fall through to the below.
}
//else fall through to the below.
} // Interception of keystrokes for navigation in tab control.
// v1.0.34: Fix for the fact that a multiline edit control will send WM_CLOSE to its parent
// when user presses ESC while it has focus. The following check is similar to the block's above.
// The alternative to this approach would have been to override the edit control's WindowProc,
// but the following seemed to be less code. Although this fix is only necessary for multiline
// edits, its done for all edits since it doesn't do any harm. In addition, there is no need to
// check what modifiers are down because we never receive the keystroke for Ctrl-Esc and Alt-Esc
// (the OS handles those beforehand) and both Win-Esc and Shift-Esc are identical to a naked Esc
// inside an edit. The following check relies heavily on short-circuit eval. order.
if ( (msg.wParam == VK_ESCAPE || msg.wParam == VK_TAB // v1.0.38.03: Added VK_TAB handling for "WantTab".
|| (msg.wParam == 'A' && (GetKeyState(VK_CONTROL) & 0x8000))) // v1.0.44: Added support for "WantCtrlA".
continue; // Omit this keystroke from any further processing.
}
} // switch()
}
if (GuiType::sTreeWithEditInProgress)
{
if (msg.wParam == VK_RETURN)
{
TreeView_EndEditLabelNow(GuiType::sTreeWithEditInProgress, FALSE); // Save changes to label/text.
continue;
}
else if (msg.wParam == VK_ESCAPE)
{
TreeView_EndEditLabelNow(GuiType::sTreeWithEditInProgress, TRUE); // Cancel without saving.
continue;
}
}
} // if (msg.message == WM_KEYDOWN)
for (i = 0, gui_count = 0, msg_was_handled = false; i < MAX_GUI_WINDOWS; ++i)
{
// Note: indications are that IsDialogMessage() should not be called with NULL as
// its first parameter (perhaps as an attempt to get allow dialogs owned by our
// thread to be handled at once). Although it might work on some versions of Windows,
// it's undocumented and shouldn't be relied on.
// Also, can't call IsDialogMessage against msg.hwnd because that is not a complete
// solution: at the very least, tab key navigation will not work in GUI windows.
// There are probably other side-effects as well.
if (g_gui[i])
{
if (g_gui[i]->mHwnd)
{
g.CalledByIsDialogMessageOrDispatch = true;
g.CalledByIsDialogMessageOrDispatchMsg = msg.message; // Added in v1.0.44.11 because it's known that IsDialogMessage can change the message number (e.g. WM_KEYDOWN->WM_NOTIFY for UpDowns)
if (IsDialogMessage(g_gui[i]->mHwnd, &msg))
{
msg_was_handled = true;
g.CalledByIsDialogMessageOrDispatch = false;
break;
}
g.CalledByIsDialogMessageOrDispatch = false;
}
if (GuiType::sGuiCount == ++gui_count) // No need to keep searching.
break;
}
}
if (msg_was_handled) // This message was handled by IsDialogMessage() above.
continue; // Continue with the main message loop.
}
// v1.0.44: There's no reason to call TRANSLATE_AHK_MSG here because all WM_COMMNOTIFY messages
// are sent to g_hWnd. Thus, our call to DispatchMessage() later below will route such messages to
// MainWindowProc(), which will then call TRANSLATE_AHK_MSG().
//TRANSLATE_AHK_MSG(msg.message, msg.wParam)
switch(msg.message)
{
// MSG_FILTER_MAX should prevent us from receiving this first group of messages whenever g_AllowInterruption or
// g.AllowThreadToBeInterrupted is false.
case AHK_HOOK_HOTKEY: // Sent from this app's keyboard or mouse hook.
case AHK_HOTSTRING: // Sent from keybd hook to activate a non-auto-replace hotstring.
case AHK_CLIPBOARD_CHANGE:
// This extra handling is present because common controls and perhaps other OS features tend
// to use WM_USER+NN messages, a practice that will probably be even more common in the future.
// To cut down on message conflicts, dispatch such messages whenever their HWND isn't a what
// it should be for a message our own code generated. That way, the target control (if any)
// will still get the msg.
if (msg.hwnd && msg.hwnd != g_hWnd) // v1.0.44: It's wasn't sent by our code; perhaps by a common control's code.
break; // Dispatch it vs. discarding it, in case it's for a control.
//ELSE FALL THROUGH:
case AHK_GUI_ACTION: // The user pressed a button on a GUI window, or some other actionable event. Listed before the below for performance.
case WM_HOTKEY: // As a result of this app having previously called RegisterHotkey(), or from TriggerJoyHotkeys().
case AHK_USER_MENU: // The user selected a custom menu item.
hdrop_to_free = NULL; // Set default for this message's processing (simplifies code).
switch(msg.message)
{
case AHK_GUI_ACTION: // Listed first for performance.
// Assume that it is possible that this message's GUI window has been destroyed
// (and maybe even recreated) since the time the msg was posted. If this can happen,
// that's another reason for finding which GUI this control is associate with (it also
// needs to be found so that we can call the correct GUI window object to perform
// the action):
if ( !(pgui = GuiType::FindGui(msg.hwnd)) ) // No associated GUI object, so ignore this event.
// v1.0.44: Dispatch vs. continue/discard since it's probably for a common control
// whose msg number happens to be AHK_GUI_ACTION. Do this *only* when HWND isn't recognized,
// not when msg content is inavalid, because dispatching a msg whose HWND is one of our own
// GUI windows might cause GuiWindowProc to fwd it back to us, creating an infinite loop.
goto break_out_of_main_switch; // Goto seems preferably in this case for code size & performance.
gui_index = pgui->mWindowIndex; // Stored in case ExecUntil() performs "Gui Destroy" further below.
gui_event_info = (DWORD)msg.lParam;
gui_action = LOWORD(msg.wParam);
gui_control_index = HIWORD(msg.wParam); // Caller has set it to NO_CONTROL_INDEX if it isn't applicable.
if (gui_action == GUI_EVENT_RESIZE) // This section be done after above but before pcontrol below.
{
gui_size = gui_event_info; // Temp storage until the "g" struct becomes available for the new thread.
gui_event_info = gui_control_index; // SizeType is stored in index in this case.
gui_control_index = NO_CONTROL_INDEX;
}
// Below relies on the GUI_EVENT_RESIZE section above having been done:
pcontrol = gui_control_index < pgui->mControlCount ? pgui->mControl + gui_control_index : NULL; // Set for use in other places below.
pgui_label_is_running = NULL; // Set default (in cases other than AHK_GUI_ACTION it is not used, so not initialized).
event_is_control_generated = false; // Set default.
switch(gui_action)
{
case GUI_EVENT_RESIZE: // This is the signal to run the window's OnEscape label. Listed first for performance.
if ( !(gui_label = pgui->mLabelForSize) ) // In case it became NULL since the msg was posted.
if ( !(gui_label = pgui->mLabelForContextMenu) ) // In case it became NULL since the msg was posted.
continue;
// UPDATE: Must allow multiple threads because otherwise the user cannot right-click twice
// consecutively (the second click is blocked because the menu is still displayed at the
// instant of the click. The following older reason is probably not entirely correct because
// the display of a popup menu via "Menu, MyMenu, Show" will spin off a new thread if the
// user selects an item in the menu:
// Unlike most other Gui labels, it seems best by default to allow GuiContextMenu to be
// launched multiple times so that multiple items in the menu can be running simultaneously
// as separate threads. Therefore, leave pgui_label_is_running at its default of NULL.
break;
case GUI_EVENT_DROPFILES: // This is the signal to run the window's DropFiles label.
hdrop_to_free = pgui->mHdrop; // This variable simplifies the code further below.
if ( !(gui_label = pgui->mLabelForDropFiles) // In case it became NULL since the msg was posted.
|| !hdrop_to_free // Checked just in case, so that the below can query it.
|| !(gui_event_info = DragQueryFile(hdrop_to_free, 0xFFFFFFFF, NULL, 0)) ) // Probably impossible, but if it ever can happen, seems best to ignore it.
{
if (hdrop_to_free) // Checked again in case short-circuit boolean above never checked it.
{
DragFinish(hdrop_to_free); // Since the drop-thread will not be launched, free the memory.
pgui->mHdrop = NULL; // Indicate that this GUI window is ready for another drop.
}
continue;
}
// It is not necessary to check if the label is running in this case because
// the caller who posted this message to us has ensured that it's the only message in the queue
// or in progress (by virtue of pgui->mHdrop being NULL at the time the message was posted).
// Therefore, leave pgui_label_is_running at its default of NULL.
break;
default: // This is an action from a particular control in the GUI window.
if (!pcontrol) // gui_control_index was beyond the quantity of controls, possibly due to parent window having been destroyed since the msg was sent (or bogus msg).
continue; // Discarding an invalid message here is relied upon both other sections below.
if ( !(gui_label = pcontrol->jump_to_label) )
{
// On if there's no label is the implicit action considered.
if (pcontrol->attrib & GUI_CONTROL_ATTRIB_IMPLICIT_CANCEL)
pgui->Cancel();
continue; // Fully handled by the above; or there was no label.
// This event might lack both a label and an action if its control was changed to be
// non-actionable since the time the msg was posted.
}
// Above has confirmed it has a label, so now it's valid to check if that label is already running.
// It seems best by default not to allow multiple threads for the same control.
// Such events are discarded because it seems likely that most script designers
// would want to see the effects of faulty design (e.g. long running timers or
// hotkeys that interrupt gui threads) rather than having events for later,
// when they might suddenly take effect unexpectedly:
if (pcontrol->attrib & GUI_CONTROL_ATTRIB_LABEL_IS_RUNNING)
continue;
event_is_control_generated = true; // As opposed to a drag-and-drop or context-menu event that targets a specific control.
// And leave pgui_label_is_running at its default of NULL because it doesn't apply to these.
} // switch(gui_action)
type_of_first_line = gui_label->mJumpToLine->mActionType; // Above would already have discarded this message if it there was no label.
break; // case AHK_GUI_ACTION
case AHK_USER_MENU: // user-defined menu item
if ( !(menu_item = g_script.FindMenuItemByID((UINT)msg.lParam)) ) // Item not found.
continue; // ignore the msg
// And just in case a menu item that lacks a label (such as a separator) is ever
// somehow selected (perhaps via someone sending direct messages to us, bypassing
// The below allows 1 thread beyond the limit in case the script's configured
// #MaxThreads is exactly equal to the absolute limit. This is because we want
// subroutines whose first line is something like ExitApp to take effect even
// when we're at the absolute limit:
if (g_nThreads > MAX_THREADS_LIMIT || !ACT_IS_ALWAYS_ALLOWED(type_of_first_line))
{
// Allow only a limited number of recursion levels to avoid any chance of
// stack overflow. So ignore this message. Later, can devise some way
// to support "queuing up" these launch-thread events for use later when
// there is "room" to run them, but that might cause complications because
// in some cases, the user didn't intend to hit the key twice (e.g. due to
// "fat fingers") and would have preferred to have it ignored. Doing such
// might also make "infinite key loops" harder to catch because the rate
// of incoming hotkeys would be slowed down to prevent the subroutines from
// running concurrently.
if (hdrop_to_free) // This is only non-NULL when pgui is non-NULL and gui_action==GUI_EVENT_DROPFILES
{
DragFinish(hdrop_to_free); // Since the drop-thread will not be launched, free the memory.
pgui->mHdrop = NULL; // Indicate that this GUI window is ready for another drop.
}
continue;
}
// If the above "continued", it seems best not to re-queue/buffer the key since
// it might be a while before the number of threads drops back below the limit.
}
// Find out the new thread's priority will be so that it can be checked against the current thread's:
switch(msg.message)
{
case AHK_GUI_ACTION: // Listed first for performance.
if (pgui_label_is_running && *pgui_label_is_running) // GuiSize/Close/Escape/etc. These subroutines are currently limited to one thread each.
continue; // hdrop_to_free: Not necessary to check it because it's always NULL when pgui_label_is_running is non-NULL.
//else the check wasn't needed because it was done elsewhere (GUI_EVENT_DROPFILES) or the
// action is not thread-restricted (GUI_EVENT_CONTEXTMENU).
// And since control-specific events were already checked for "already running" higher above, this
// event is now eligible to start a new thread.
priority = 0; // Always use default for now.
break;
case AHK_USER_MENU: // user-defined menu item
// Ignore/discard a hotkey or custom menu item event if the current thread's priority
// is higher than it's:
priority = menu_item->mPriority;
// Below: the menu type is passed with the message so that its value will be in sync
// with the timestamp of the message (in case this message has been stuck in the
// queue for a long time):
if (msg.wParam < MAX_GUI_WINDOWS) // Poster specified that this menu item was from a gui's menu bar (since wParam is unsigned, any incoming -1 is seen as greater than max).
{
// msg.wParam is the index rather than a pointer to avoid any chance of problems with
// a gui object or its window having been destroyed while the msg was waiting in the queue.
if (!(pgui = g_gui[msg.wParam]) // Not a GUI's menu bar item...
&& msg.hwnd && msg.hwnd != g_hWnd) // ...and not a script menu item.
goto break_out_of_main_switch; // See "goto break_out_of_main_switch" higher above for complete explanation.
}
else
pgui = NULL; // Set for use in later sections.
break;
case AHK_HOTSTRING:
priority = hs->mPriority;
break;
case AHK_CLIPBOARD_CHANGE: // Due to the presence of an OnClipboardChange label in the script.
if (g_script.mOnClipboardChangeIsRunning)
continue;
priority = 0; // Always use default for now.
break;
default: // hotkey
// Due to the key-repeat feature and the fact that most scripts use a value of 1
// for their #MaxThreadsPerHotkey, this check will often help average performance
// by avoiding a lot of unncessary overhead that would otherwise occur:
if (!hk->PerformIsAllowed(*variant))
{
// The key is buffered in this case to boost the responsiveness of hotkeys
// that are being held down by the user to activate the keyboard's key-repeat
// feature. This way, there will always be one extra event waiting in the queue,
// which will be fired almost the instant the previous iteration of the subroutine
// finishes (this above descript applies only when MaxThreadsPerHotkey is 1,
// which it usually is).
hk->RunAgainAfterFinished(*variant); // Wheel notch count (g.EventInfo below) should be okay because subsequent launches reuse the same thread attributes to do the repeats.
continue;
}
priority = variant->mPriority;
}
// Discard the event if it's priority is lower than that of the current thread:
if (priority < g.Priority)
{
if (hdrop_to_free) // This is only non-NULL when pgui is non-NULL and gui_action==GUI_EVENT_DROPFILES
{
DragFinish(hdrop_to_free); // Since the drop-thread will not be launched, free the memory.
pgui->mHdrop = NULL; // Indicate that this GUI window is ready for another drop.
}
continue;
}
// Now it is certain that the new thread will be launched, so set everything up.
// Perform the new thread's subroutine:
return_value = true; // We will return this value to indicate that we launched at least one new thread.
// Always kill the main timer, for performance reasons and for simplicity of design,
// prior to embarking on new subroutine whose duration may be long (e.g. if BatchLines
// is very high or infinite, the called subroutine may not return to us for seconds,
// minutes, or more; during which time we don't want the timer running because it will
// only fill up the queue with WM_TIMER messages and thus hurt performance).
// UPDATE: But don't kill it if it should be always-on to support the existence of
// at least one enabled timed subroutine or joystick hotkey:
if (!g_script.mTimerEnabledCount && !Hotkey::sJoyHotkeyCount)
KILL_MAIN_TIMER;
switch(msg.message)
{
case AHK_GUI_ACTION: // Listed first for performance.
case AHK_CLIPBOARD_CHANGE:
break; // Do nothing at this stage.
case AHK_USER_MENU: // user-defined menu item
// Safer to make a full copies than point to something potentially volatile.
//else pcontrol==NULL: Since there is no focused control, it seems best to report the
// cursor's position rather than some arbitrary center-point, or top-left point in the
// parent window. This is because it might be more convenient for the user to move the
// mouse to select a menu item (since menu will be close to mouse cursor).
ScreenToWindow(g.GuiPoint, pgui->mHwnd); // For compatibility with "Menu Show", convert to window coordinates. A CoordMode option can be added to change this if desired.
break; // case GUI_CONTEXT_MENU.
case GUI_EVENT_DROPFILES:
g.GuiEvent = gui_action; // i.e. set to GUI_EVENT_DROPFILES for special use by GetGuiEvent().
g.GuiPoint = msg.pt; // v1.0.38: More accurate/customary to use msg.pt than GetCursorPos().
ScreenToWindow(g.GuiPoint, pgui->mHwnd);
// Visually indicate that drops aren't allowed while and existing drop is still being
// processed. Fix for v1.0.31.02: The window's current ExStyle is fetched every time
// in case a non-GUI command altered it (such as making it transparent):
case GUI_CONTROL_STATUSBAR: // An earlier stage has ensured pcontrol isn't NULL in this case.
// For performance reasons, this isn't done for all GUI events, just ones that
// have a typical use for the coords.
g.GuiPoint = msg.pt;
ScreenToWindow(g.GuiPoint, pgui->mHwnd);
break;
case GUI_CONTROL_LISTVIEW: // v1.0.46.10: Added this section to support notifying the script of HOW the item changed.
if (LOBYTE(gui_action) == 'I')
{
walk = gui_action_errorlevel;
if (gui_action & AHK_LV_SELECT) // Keep this one first, and the others below in the same order, in case any scripts come to rely on the ordering of the letters within the string.
*walk++ = 'S';
else if (gui_action & AHK_LV_DESELECT)
*walk++ = 's';
if (gui_action & AHK_LV_FOCUS)
*walk++ = 'F';
else if (gui_action & AHK_LV_DEFOCUS)
*walk++ = 'f';
if (gui_action & AHK_LV_CHECK)
*walk++ = 'C';
else if (gui_action & AHK_LV_UNCHECK)
*walk++ = 'c';
// Search on "AHK_LV_DROPHILITE" for comments about why the below is commented out:
//if (gui_action & AHK_LV_DROPHILITE)
// *walk++ = 'D';
//else if (gui_action & AHK_LV_UNDROPHILITE)
// *walk++ = 'd';
*walk = '\0'; // Provide terminator inside gui_action_errorlevel.
gui_action = 'I'; // Done only after we're done using it above. This clears out the flags above to leave only a naked 'I'.
}
break;
//default: No action for any other control-generated events since caller already set things up properly.
}
g.GuiEvent = gui_action; // Set g.GuiEvent to indicate whether a double-click or other non-standard event launched it.
} // switch (msg.message)
// We're still in case AHK_GUI_ACTION; other cases have their own handling for g.EventInfo.
// gui_event_info is a separate variable because it is sometimes set before g.EventInfo is available
// for the new thread.
// v1.0.44: For the following reasons, make ErrorLevel mirror A_EventInfo only when it
// is documented to do so for backward compatibility:
// 1) Avoids slight performance drain of having to convert a number to text and store it in ErrorLevel.
// 2) Reserves ErrorLevel for potential future uses.
if (gui_action == GUI_EVENT_RESIZE || gui_action == GUI_EVENT_DROPFILES)
g_ErrorLevel->Assign(gui_event_info); // For backward compatibility.
else
g_ErrorLevel->Assign(gui_action_errorlevel); // Helps reserve it for future use. See explanation above.
// Set last found window (as documented). It's not necessary to check IsWindow/IsWindowVisible/
// DetectHiddenWindows since GetValidLastUsedWindow() takes care of that whenever the script
// actually tries to use the last found window. UPDATE: Definitely don't want to check
// IsWindowVisible/DetectHiddenWindows now that the last-found window is exempt from
// DetectHiddenWindows if the last-found window is one of the script's GUI windows [v1.0.25.13]:
g.hWndLastUsed = pgui->mHwnd;
g.GuiWindowIndex = pgui->mWindowIndex;
g.GuiDefaultWindowIndex = pgui->mWindowIndex; // GUI threads default to operating upon their own window.
g.GuiControlIndex = gui_control_index; // Must be set only after the "g" struct has been initialized. This will be NO_CONTROL_INDEX if the sender of the message said to do that.
g.EventInfo = gui_event_info; // Override the thread-default of NO_EVENT_INFO.
if (pgui_label_is_running) // i.e. GuiClose, GuiEscape, and related window-level events.
*pgui_label_is_running = true;
else if (event_is_control_generated) // An earlier stage has ensured pcontrol isn't NULL in this case.
pcontrol->attrib |= GUI_CONTROL_ATTRIB_LABEL_IS_RUNNING; // Must be careful to set this flag only when the event is control-generated, not for a drag-and-drop onto the control, or context menu on the control, etc.
// LAUNCH GUI THREAD:
gui_label->Execute();
// Bug-fix for v1.0.22: If the above ExecUntil() performed a "Gui Destroy", the
// pointers below are now invalid so should not be dereferenced. In such a case,
// hdrop_to_free will already have been freed as part of the window destruction
// process, so don't do it here. g_gui[gui_index] is checked to ensure the window
// still exists:
if (pgui = g_gui[gui_index]) // Assign. This refresh is a bug-fix as explained below.
{
// Bug-fix for v1.0.30.04: If the thread that was just launched above destroyed
// its own GUI window, but then recreated it, that window's members obviously aren't
// guaranteed to have the same memory addresses that they did prior to destruction.
// Even g_gui[gui_index] would probably be a different address, so pgui would be
// invalid too. Therefore, refresh the original pointers (pgui is refreshed above).
// See similar switch() higher above for comments about the below:
} // if (this gui window wasn't destroyed-without-recreation by the thread we just launched).
break;
case AHK_USER_MENU: // user-defined menu item
// Below: the menu type is passed with the message so that its value will be in sync
// with the timestamp of the message (in case this message has been stuck in the
// queue for a long time):
if (pgui) // Set by an earlier stage. It means poster specified that this menu item was from a gui's menu bar.
{
// As documented, set the last found window if possible/applicable. It's not necessary to
// check IsWindow/IsWindowVisible/DetectHiddenWindows since GetValidLastUsedWindow()
// takes care of that whenever the script actually tries to use the last found window.
g.hWndLastUsed = pgui->mHwnd; // OK if NULL.
// This flags GUI menu items as being GUI so that the script has a way of detecting
// whether a given submenu's item was selected from inside a menu bar vs. a popup:
g.GuiEvent = GUI_EVENT_NORMAL;
g.GuiWindowIndex = g.GuiDefaultWindowIndex = pgui->mWindowIndex; // But leave GuiControl at its default, which flags this event as from a menu item.
}
menu_item->mLabel->Execute();
break;
case AHK_HOTSTRING:
g.hWndLastUsed = criterion_found_hwnd; // v1.0.42. Even if the window is invalid for some reason, IsWindow() and such are called whenever the script accesses it (GetValidLastUsedWindow()).
g_ErrorLevel->Assign(g.EventInfo); // For backward compatibility.
// ACT_IS_ALWAYS_ALLOWED() was already checked above.
// The message poster has ensured that g_script.mOnClipboardChangeLabel is non-NULL and valid.
g_script.mOnClipboardChangeIsRunning = true;
g_script.mOnClipboardChangeLabel->Execute();
g_script.mOnClipboardChangeIsRunning = false;
break;
default: // hotkey
if (hk->mVK == VK_WHEEL_DOWN || hk->mVK == VK_WHEEL_UP) // If this is true then also: msg.message==AHK_HOOK_HOTKEY
g.EventInfo = (DWORD)msg.lParam; // v1.0.43.03: Override the thread default of 0 with the number of notches by which the wheel was turned.
// Above also works for RunAgainAfterFinished since that feature reuses the same thread attributes set above.
g.hWndLastUsed = criterion_found_hwnd; // v1.0.42. Even if the window is invalid for some reason, IsWindow() and such are called whenever the script accesses it (GetValidLastUsedWindow()).
hk->Perform(*variant);
}
// v1.0.37.06: Call ResumeUnderlyingThread() even if aMode==WAIT_FOR_MESSAGES; this is for
// maintainability and also in case the pause command has been used to unpause the idle thread.
if (aMode == WAIT_FOR_MESSAGES) // This is the "idle thread", meaning that the end of the thread above has returned the script to an idle state.
{
// v1.0.38.04: The following line is for maintainability and reliability. It avoids the need
// for other sections to figure out whether they should reset g.ThreadIsCritical to false
// when a thread is resumed (a resumed thread might still be critical if it was interrupted
// by an emergency thread such as OnExit or OnMessage).
g.AllowThreadToBeInterrupted = true; // This one is probably necessary due to the way it conforms to ThreadIsCritical in other sections.
g.ThreadIsCritical = false; // Not strictly necessary but improves maintainability.
g.AllowTimers = true; // Same as above.
g.Priority = PRIORITY_MINIMUM; // Ensure minimum priority so that idle state can always be "interrupted".
}
else // Some thread other than the idle thread.
{
if (IsCycleComplete(aSleepDuration, start_time, allow_early_return))
{
// Check for messages once more in case the subroutine that just completed
// above didn't check them that recently. This is done to minimize the time
// our thread spends *not* pumping messages, which in turn minimizes keyboard
// and mouse lag if the hooks are installed (even though this is no longer
// true due to v1.0.39's dedicated hook thread, it seems best to continue this
// practice to maximize responsiveness of hotkeys, the app itself [e.g. tray
// menu], and also to retain backward compatibility). Set the state of this
// function/layer/instance so that it will use peek-mode. UPDATE: Don't change
// the value of aSleepDuration to -1 because IsCycleComplete() needs to know the
// original sleep time specified by the caller to determine whether
// to decrement g_nLayersNeedingTimer:
empty_the_queue_via_peek = true;
allow_early_return = true;
// And now let it fall through to the "continue" statement below.
}
else if (this_layer_needs_timer) // Ensure the timer is back on since we still need it here.
SET_MAIN_TIMER // This won't do anything if it's already on.
// and now if the cycle isn't complete, stay in the blessed GetMessage() state until the time
// has expired.
}
continue;
// End of cases that launch new threads, such as hotkeys and GUI events.
case WM_TIMER:
if (msg.lParam // This WM_TIMER is intended for a TimerProc...
|| msg.hwnd != g_hWnd) // ...or its intended for a window other than the main window, which implies that it doesn't belong to program internals (i.e. the script is probably using it). This fix was added in v1.0.47.02 and it also fixes the ES_NUMBER balloon bug.
break; // Fall through to let a later section do DispatchMessage() on it.
// It seems best to poll the joystick for every WM_TIMER message (i.e. every 10ms or so on
// NT/2k/XP). This is because if the system is under load, it might be 20ms, 40ms, or even
// longer before we get a timeslice again and that is a long time to be away from the poll
// (a fast button press-and-release might occur in less than 50ms, which could be missed if
// the polling frequency is too low):
POLL_JOYSTICK_IF_NEEDED // Do this first since it's much faster.
// v1.0.38.04: The following line is done prior to the timer-check to reduce situations
// in which a timer thread is interrupted before it can execute even a single line.
// Search for mLastPeekTime in MsgSleep() for detailed explanation.
g_script.mLastPeekTime = GetTickCount(); // It's valid to reset this because by definition, "msg" just came in via Get() or Peek(), both of which qualify as a Peek() for this purpose.
CHECK_SCRIPT_TIMERS_IF_NEEDED
if (aMode == WAIT_FOR_MESSAGES)
// Timer should have already been killed if we're in this state.
// But there might have been some WM_TIMER msgs already in the queue
// (they aren't removed when the timer is killed). Or perhaps
// a caller is calling us with this aMode even though there
// are suspended subroutines (currently never happens).
// In any case, these are always ignored in this mode because
// the caller doesn't want us to ever return. UPDATE: This can now
// happen if there are any enabled timed subroutines we need to keep an
// eye on, which is why the mTimerEnabledCount value is checked above
// prior to starting a new iteration.
continue;
if (aSleepDuration < 1) // In this case, WM_TIMER messages have already fulfilled their function, above.
continue;
// Otherwise aMode == RETURN_AFTER_MESSAGES:
// Realistically, there shouldn't be any more messages in our queue
// right now because the queue was stripped of WM_TIMER messages
// prior to the start of the loop, which means this WM_TIMER
// came in after the loop started. So in the vast majority of
// cases, the loop would have had enough time to empty the queue
// prior to this message being received. Therefore, just return rather
// than trying to do one final iteration in peek-mode (which might
// complicate things, i.e. the below function makes certain changes
// in preparation for ending this instance/layer, only to be possibly,
// but extremely rarely, interrupted/recursed yet again if that final
// peek were to detect a recursable message):
if (IsCycleComplete(aSleepDuration, start_time, allow_early_return))
RETURN_FROM_MSGSLEEP
// Otherwise, stay in the blessed GetMessage() state until the time has expired:
continue;
case WM_CANCELJOURNAL:
// IMPORTANT: It's tempting to believe that WM_CANCELJOURNAL might be lost/dropped if the script
// is displaying a MsgBox or other dialog that has its own msg pump (since such a pump would
// discard any msg with a NULL HWND). However, that is not true in this case because such a dialog's
// msg pump would be beneath this one on the call stack. This is because our caller is calling us in
// a loop that does not permit the script to display any *additional* dialogs. Thus, our msg pump
// here should always receive the OS-generated WM_CANCELJOURNAL msg reliably.
// v1.0.44: This message is now received only when the user presses Ctrl-Alt-Del or Ctrl-Esc during
// playback. For performance and simplicity, the playback hook itself no longer sends this message,
// instead directly sets g_PlaybackHook = NULL to notify the installer of the hook that it's gone.
g_PlaybackHook = NULL; // A signal for caller.
empty_the_queue_via_peek = true;
// Above is set to so that we return faster, since our caller should be SendKeys() whenever
// WM_CANCELJOURNAL is received, and SendKeys() benefits from a faster return.
continue;
case WM_KEYDOWN:
if (msg.hwnd == g_hWndEdit && msg.wParam == VK_ESCAPE)
{
// This won't work if a MessageBox() window is displayed because its own internal
// message pump will dispatch the message to our edit control, which will just
// ignore it. And avoiding setting the focus to the edit control won't work either
// because the user can simply click in the window to set the focus. But for now,
// this is better than nothing:
ShowWindow(g_hWnd, SW_HIDE); // And it's okay if this msg gets dispatched also.
continue;
}
// Otherwise, break so that the messages will get dispatched. We need the other
// WM_KEYDOWN msgs to be dispatched so that the cursor is keyboard-controllable in
// the edit window:
break;
case WM_QUIT:
// The app normally terminates before WM_QUIT is ever seen here because of the way
// WM_CLOSE is handled by MainWindowProc(). However, this is kept here in case anything
// external ever explicitly posts a WM_QUIT to our thread's queue:
g_script.ExitApp(EXIT_WM_QUIT);
continue; // Since ExitApp() won't necessarily exit.
} // switch()
break_out_of_main_switch:
// If a "continue" statement wasn't encountered somewhere in the switch(), we want to
// process this message in a more generic way.
// This little part is from the Miranda source code. But it doesn't seem
// to provide any additional functionality: You still can't use keyboard
// keys to navigate in the dialog unless it's the topmost dialog.
// UPDATE: The reason it doesn't work for non-topmost MessageBoxes is that
// this message pump isn't even the one running. It's the pump of the
// top-most MessageBox itself, which apparently doesn't properly dispatch
// all types of messages to other MessagesBoxes. However, keeping this
// here is probably a good idea because testing reveals that it does
// sometimes receive messages intended for MessageBox windows (which makes
// sense because our message pump here retrieves all thread messages).
// It might cause problems to dispatch such messages directly, since
// IsDialogMessage() is supposed to be used in lieu of DispatchMessage()
// for these types of messages.
// NOTE: THE BELOW IS CONFIRMED to be needed, at least for a FileSelectFile()
// dialog whose quasi-thread has been suspended, and probably for some of the other
// types of dialogs as well:
if ((fore_window = GetForegroundWindow()) != NULL // There is a foreground window.
&& GetWindowThreadProcessId(fore_window, NULL) == g_MainThreadID) // And it belongs to our main thread (the main thread is the only one that owns any windows).
if (!strcmp(wnd_class_name, "#32770")) // MsgBox, InputBox, FileSelectFile/Folder dialog.
{
g.CalledByIsDialogMessageOrDispatch = true; // In case there is any way IsDialogMessage() can call one of our own window proc's rather than that of a MsgBox, etc.
g.CalledByIsDialogMessageOrDispatchMsg = msg.message; // Added in v1.0.44.11 because it's known that IsDialogMessage can change the message number (e.g. WM_KEYDOWN->WM_NOTIFY for UpDowns)
if (IsDialogMessage(fore_window, &msg)) // This message is for it, so let it process it.
{
// If it is likely that a FileSelectFile dialog is active, this
// section attempt to retain the current directory as the user
// navigates from folder to folder. This is done because it is
// possible that our caller is a quasi-thread other than the one
// that originally launched the FileSelectFile (i.e. that dialog
// is in a suspended thread), in which case the user's navigation
// would cause the active threads working dir to change unexpectedly
// unless the below is done. This is not a complete fix since if
// a message pump other than this one is running (e.g. that of a
// MessageBox()), these messages will not be detected:
if (g_nFileDialogs) // See MsgSleep() for comments on this.
// The below two messages that are likely connected with a user
// navigating to a different folder within the FileSelectFile dialog.
// That avoids changing the directory for every message, since there
// can easily be thousands of such messages every second if the
// user is moving the mouse. UPDATE: This doesn't work, so for now,
// just call SetCurrentDirectory() for every message, which does work.
// A brief test of CPU utilization indicates that SetCurrentDirectory()
// is not a very high overhead call when it is called many times here:
continue; // This message is done, so start a new iteration to get another msg.
}
g.CalledByIsDialogMessageOrDispatch = false;
}
}
// Translate keyboard input for any of our thread's windows that need it:
if (!g_hAccelTable || !TranslateAccelerator(g_hWnd, g_hAccelTable, &msg))
{
g.CalledByIsDialogMessageOrDispatch = true; // Relies on the fact that the types of messages we dispatch can't result in a recursive call back to this function.
g.CalledByIsDialogMessageOrDispatchMsg = msg.message; // Added in v1.0.44.11. Do it prior to Translate & Dispatch in case either one of them changes the message number (it is already known that IsDialogMessage can change message numbers).
TranslateMessage(&msg);
DispatchMessage(&msg); // This is needed to send keyboard input and other messages to various windows and for some WM_TIMERs.
// But never kill the main timer, since the mere fact that we're here means that
// there's at least one enabled timed subroutine. Though later, performance can
// be optimized by killing it if there's exactly one enabled subroutine, or if
// all the subroutines are already in a running state (due to being buried beneath
// the current quasi-thread). However, that might introduce unwanted complexity
// in other places that would need to start up the timer again because we stopped it, etc.
}
// Fix for v1.0.31: mTimeLastRun is now given its new value *before* the thread is launched
// rather than after. This allows a timer to be reset by its own thread -- by means of
// "SetTimer, TimerName", which is otherwise impossible because the reset was being
// overridden by us here when the thread finished.
// Seems better to store the start time rather than the finish time, though it's clearly
// debatable. The reason is that it's sometimes more important to ensure that a given
// timed subroutine is *begun* at the specified interval, rather than assuming that
// the specified interval is the time between when the prior run finished and the new
// one began. This should make timers behave more consistently (i.e. how long a timed
// subroutine takes to run SHOULD NOT affect its *apparent* frequency, which is number
// of times per second or per minute that we actually attempt to run it):
timer.mTimeLastRun = tick_start;
if (timer.mRunOnlyOnce)
timer.Disable(); // This is done prior to launching the thread for reasons similar to above.
++launched_threads;
if (g_nFileDialogs) // See MsgSleep() for comments on this.
SetCurrentDirectory(g_WorkingDir);
// Note that it is not necessary to call UpdateTrayIcon() here since timers won't run
// if there is any paused thread, thus the icon can't currently be showing "paused".
// This next line is necessary in case a prior iteration of our loop invoked a different
// timed subroutine that changed any of the global struct's values. In other words, make
// every newly launched subroutine start off with the global default values that
// the user set up in the auto-execute part of the script (e.g. KeyDelay, WinDelay, etc.).
// However, we do not set ErrorLevel to NONE here because it's more flexible that way
// (i.e. the user may want one hotkey subroutine to use the value of ErrorLevel set by another):
InitNewThread(timer.mPriority, false, false, timer.mLabel->mJumpToLine->mActionType); // False as last param because ++g_nThreads should be done only once rather than each Init().
// The above also resets g_script.mLinesExecutedThisCycle to zero, which should slightly
// increase the expectation that any short timed subroutine will run all the way through
// to completion rather than being interrupted by the press of a hotkey, and thus potentially
// buried in the stack. However, mLastScriptRest is not set to GetTickCount() here because
// unlike other events -- which are typically in response to an explicit action by the user
// such as pressing a button or hotkey -- timers are lower priority and more relaxed.
// Also, mLastScriptRest really should only be set when a call to Get/PeekMsg has just
// occurred, so it should be left as the responsibilty of the section in MsgSleep that
// launches new threads.
++timer.mExistingThreads;
timer.mLabel->Execute();
--timer.mExistingThreads;
KILL_UNINTERRUPTIBLE_TIMER
} // if timer is due to launch.
} // for() each timer.
if (launched_threads) // Since at least one subroutine was run above, restore various values for our caller.
{
ResumeUnderlyingThread(&global_saved, ErrorLevel_saved, false); // Last param "false" because KILL_UNINTERRUPTIBLE_TIMER was already done above.
return true;
}
return false;
}
void PollJoysticks()
// It's best to call this function only directly from MsgSleep() or when there is an instance of
// MsgSleep() closer on the call stack than the nearest dialog's message pump (e.g. MsgBox).
// This is because events posted to the thread indirectly by us here would be discarded or mishandled
// by a non-standard (dialog) message pump.
//
// Polling the joysticks this way rather than using joySetCapture() is preferable for several reasons:
// 1) I believe joySetCapture() internally polls the joystick anyway, via a system timer, so it probably
// doesn't perform much better (if at all) than polling "manually".
// 2) joySetCapture() only supports 4 buttons;
// 3) joySetCapture() will fail if another app is already capturing the joystick;
// 4) Even if the joySetCapture() succeeds, other programs (e.g. older games), would be prevented from
// capturing the joystick while the script in question is running.
{
// Even if joystick hotkeys aren't currently allowed to fire, poll it anyway so that hotkey
// messages can be buffered for later.
static DWORD sButtonsPrev[MAX_JOYSTICKS] = {0}; // Set initial state to "all buttons up for all joysticks".
JOYINFOEX jie;
DWORD buttons_newly_down;
for (int i = 0; i < MAX_JOYSTICKS; ++i)
{
if (!Hotkey::sJoystickHasHotkeys[i])
continue;
// Reset these every time in case joyGetPosEx() ever changes them. Also, most systems have only one joystick,
// so this code will hardly ever be executed more than once (and sometimes zero times):
jie.dwSize = sizeof(JOYINFOEX);
jie.dwFlags = JOY_RETURNBUTTONS; // vs. JOY_RETURNALL
if (joyGetPosEx(i, &jie) != JOYERR_NOERROR) // Skip this joystick and try the others.
continue;
// The exclusive-or operator determines which buttons have changed state. After that,
// the bitwise-and operator determines which of those have gone from up to down (the
// down-to-up events are currently not significant).
if (monitor.instance_count >= monitor.max_instances || g.Priority > 0) // Monitor is already running more than the max number of instances, or existing thread's priority is too high to be interrupted.
return false;
// Need to check if backup is needed in case script explicitly called the function rather than using
// it solely as a callback. UPDATE: And now that max_instances is supported, also need it for that.
// See ExpandExpression() for detailed comments about the following section.
VarBkp *var_backup = NULL; // If needed, it will hold an array of VarBkp objects.
int var_backup_count; // The number of items in the above array.
if (func.mInstances > 0) // Backup is needed.
if (!Var::BackupFunctionVars(func, var_backup, var_backup_count)) // Out of memory.
return false;
// Since we're in the middle of processing messages, and since out-of-memory is so rare,
// it seems justifiable not to have any error reporting and instead just avoid launching
// the new thread.
// Since above didn't return, the launch of the new thread is now considered unavoidable.
// See MsgSleep() for comments about the following section.
// Set last found window (as documented). Can be NULL.
// Nested controls like ComboBoxes require more than a simple call to GetParent().
if (g.hWndLastUsed = GetNonChildParent(aWnd)) // Assign parent window as the last found window (it's ok if it's hidden).
{
GuiType *pgui = GuiType::FindGui(g.hWndLastUsed);
if (pgui) // This parent window is a GUI window.
{
g.GuiWindowIndex = pgui->mWindowIndex; // Update the built-in variable A_GUI.
g.GuiDefaultWindowIndex = pgui->mWindowIndex; // Consider this a GUI thread; so it defaults to operating upon its own window.
GuiIndexType control_index = (GuiIndexType)(size_t)pgui->FindControl(aWnd, true); // v1.0.44.03: Call FindControl() vs. GUI_HWND_TO_INDEX so that a combobox's edit control is properly resolved to the combobox itself.
if (control_index < pgui->mControlCount) // Match found (relies on unsigned for out-of-bounds detection).
g.GuiControlIndex = control_index;
//else leave it at its default, which was set when the new thread was initialized.
}
//else leave the above members at their default values set when the new thread was initialized.
}
if (apMsg)
{
g.GuiPoint = apMsg->pt;
g.EventInfo = apMsg->time;
}
//else leave them at their init-thread defaults.
// See ExpandExpression() for detailed comments about the following section.
if (func.mParamCount > 0)
{
// Copy the appropriate values into each of the function's formal parameters.
if (func.mParamCount > 1) // Assign parameter #2: lParam
{
// v1.0.38.01: LPARAM is now written out as a DWORD because the majority of system messages
// use LPARAM as a pointer or other unsigned value. This shouldn't affect most scripts because
// of the way ATOI64() and ATOU() wrap a negative number back into the unsigned domain for
// commands such as PostMessage/SendMessage.
func.mParam[1].var->Assign((DWORD)alParam);
if (func.mParamCount > 2) // Assign parameter #3: Message number (in case this function monitors more than one).
{
func.mParam[2].var->AssignHWND((HWND)(size_t)aMsg); // Write msg number as hex because it's a lot more common. Casting issues make it easier to retain the name "AssignHWND".
if (func.mParamCount > 3) // Assign parameter #4: HWND (listed last since most scripts won't use it for anything).
func.mParam[3].var->AssignHWND(aWnd); // Can be a parent or child window.
}
}
}
// v1.0.38.04: Below was added to maximize responsiveness to incoming messages. The reasoning
// is similar to why the same thing is done in MsgSleep() prior to its launch of a thread, so see
// Fix for v1.0.47: Must handle return_value BEFORE calling FreeAndRestoreFunctionVars() because return_value
// might be the contents of one of the function's local variables (which are about to be free'd).
bool block_further_processing = *return_value; // No need to check the following because they're implied for *return_value!=0: result != EARLY_EXIT && result != FAIL;
if (block_further_processing)
aMsgReply = (LPARAM)ATOI64(return_value); // Use 64-bit in case it's an unsigned number greater than 0x7FFFFFFF, in which case this allows it to wrap around to a negative.
//else leave aMsgReply uninitialized because we'll be returning false later below, which tells our caller
// Check that the msg_index item still exists (it may have been deleted during the thread that just finished,
// either by the thread itself or some other thread that interrupted it). BIF_OnMessage has been sure to
// reset deleted array elements to have a NULL func. Even so, the following scenario could happen:
// 1) The message element is deleted.
// 2) It is recreated to be the same as before, but now it has a different array index.
// 3) It's instance_count member would have been set to 0 upon creation, and the thread for the same
// message might have launched the same function we did above, or some other.
// 4) Everything seems okay in this case, especially given its rarity.
//
// But what if step 2 above created the same msg+func in the same position as before? It's instance_count
// member would have been wrongly decremented, which would have allowed this msg-monitor thread to launch
// a thread beyond max-threads while it was techically still running above. This scenario seems too rare
// and the consequences too small to justify the extra code size, so it is documented here as a known limitation.
//
// Thus, if "monitor" is defunct due to deletion, decrementing its instance_count is harmless.
// However, "monitor" might have been reused by BIF_OnMessage() to create a new msg-monitor, so the
// thing that must be checked is the message number to avoid wrongly decrementing some other msg-monitor's
// instance_count. Update: Check g_MsgMonitorCount in case it has shrunk (which could leave
// "monitor" pointing to an element in the array that is now unused/obsolete).
if (g_MsgMonitorCount >= msg_count_orig && monitor.msg == aMsg)
{
if (monitor.instance_count) // Avoid going negative, which might otherwise be possible in weird circumstances described in other comments.
--monitor.instance_count;
}
else // "monitor" is now some other msg-monitor (or an obsolete item in array), so do don't change it (see above comments).
{
// Fix for v1.0.44.10: If OnMessage is called from *inside* some other monitor function in a way that
// deletes a message monitor, monitor.instance_count wouldn't get decremented (but only if the
// message(s) that were deleted lay to the left of it in the array). So check if the monitor is
// somewhere else in the array and if found (i.e. it didn't delete itself), update it.
for (msg_index = 0; msg_index < g_MsgMonitorCount; ++msg_index)
if (g_MsgMonitor[msg_index].msg == aMsg)
{
if (g_MsgMonitor[msg_index].instance_count) // Avoid going negative, which might otherwise be possible in weird circumstances described in other comments.
--g_MsgMonitor[msg_index].instance_count;
break;
}
}
return block_further_processing; // If false, the caller will ignore aMsgReply and process this message normally. If true, aMsgReply contains the reply the caller should immediately send for this message.